#include "soundtrack.h"

#include <QDebug>

#define SEEK_THRESHOLD 200 //Corresponds to approx. 50 fps

SoundTrack::SoundTrack(QObject *parent) :
    QAbstractAnimation(parent),
    _duration(0),
    _prevTime(0)
{}

SoundTrack::~SoundTrack(){
    int i;

    //Free the memory used by the Phonon media components
    for(i = 0; i < _sounds.count(); i++){
        delete _sounds[i].mediaObject;
        delete _sounds[i].audioOutput;
    }
}

void SoundTrack::updateCurrentTime(int msec){
    int i, timestamp, seekPosition;
    bool inMedia;
    Phonon::MediaObject* curMediaObject;
    SoundObject curSound;

    for(i = 0; i < _sounds.count(); i++){
        curSound = _sounds[i];
        timestamp = curSound.timestamp;
        curMediaObject = curSound.mediaObject;

        inMedia = ((msec >= timestamp) && (msec < (timestamp + curSound.duration)));
        seekPosition = (msec - timestamp) + curSound.trimStart;

        if(state() == QAbstractAnimation::Running){
            if(abs(msec - _prevTime) < SEEK_THRESHOLD){
                //Normal playback
                if(inMedia && curMediaObject->state() != Phonon::PlayingState){
                    //We haven't started playing the sound yet
                    curSound.mediaObject->seek(seekPosition);
                    curSound.mediaObject->play();
                    emit soundPlayed(curSound.timestamp, curMediaObject->currentSource().fileName());
                }
            }
            else{
                //Seeking
                if(inMedia){
                    curSound.mediaObject->seek(seekPosition);
                }
                else{
                    //We have seeked past the end of a playing sound, stop it
                    curSound.mediaObject->stop();
                }
            }
        }
    }

    _prevTime = msec;
}


void SoundTrack::updateState(QAbstractAnimation::State newState, QAbstractAnimation::State oldState){
    int i;

    if(newState == QAbstractAnimation::Paused){
        for(i = 0; i < _sounds.count(); i++){
            _sounds[i].mediaObject->pause();
        }
    }
    else if(newState == QAbstractAnimation::Stopped){
        for(i = 0; i < _sounds.count(); i++){
            if(_sounds[i].mediaObject->state() != Phonon::StoppedState)
                _sounds[i].mediaObject->stop();
        }
    }
}


int SoundTrack::duration() const{
    //We are calculating the duration only when it changes and storing it as a variable.
    //This may seem a little more complicated than just calculating it every
    //time duration() is called, but this function is called on every tick,
    //so it would be terribly inefficient to be calculating it every time.
    return _duration;
}

void SoundTrack::setSoundAt(int msec, QString filename){
    int endTime, prevDuration = _duration;
    bool exists = false;
    SoundObject newSound;

    //Find out if a sound already exists at the given time
    for(int i = 0; i < _sounds.count(); i++){
        if(_sounds[i].timestamp == msec){
            qDebug() << "Sound already exists";
            newSound = _sounds[i];
            _sounds[i].mediaObject->setCurrentSource(Phonon::MediaSource(filename));
            _sounds[i].trimStart = 0;
            _sounds[i].duration = _sounds[i].mediaObject->totalTime();

            exists = true;

            emit soundSet(msec, newSound.mediaObject->currentSource().fileName());
            break;
        }
    }
    if(!exists){
        qDebug() << "Sound doesn't exist";
        //Create a new Phonon media object for the sound
        Phonon::MediaObject* mediaObject = new Phonon::MediaObject(this);
        Phonon::AudioOutput* audioOutput = new Phonon::AudioOutput(Phonon::GameCategory, this);
        Phonon::AudioDataOutput* dataOutput = new Phonon::AudioDataOutput(this);

        Phonon::createPath(mediaObject, audioOutput);

        connect(mediaObject, SIGNAL(totalTimeChanged(qint64)),
                this, SLOT(onTotalTimeChanged(qint64)));
        connect(mediaObject, SIGNAL(prefinishMarkReached(qint32)),
                this, SLOT(onPreFinishMark(qint32)));
        connect(mediaObject, SIGNAL(finished()),
                this, SLOT(onFinished()));
        connect(mediaObject, SIGNAL(stateChanged(Phonon::State,Phonon::State)),
                this, SLOT(onStateChanged(Phonon::State,Phonon::State)));

        connect(dataOutput, SIGNAL(dataReady(QMap<Phonon::AudioDataOutput::Channel,QVector<qint16> >)),
                this, SLOT(onDataReady(QMap<Phonon::AudioDataOutput::Channel,QVector<qint16> >)));

        mediaObject->setCurrentSource(Phonon::MediaSource(filename));

        newSound.timestamp = msec;
        newSound.duration = mediaObject->totalTime();
        newSound.mediaDuration = mediaObject->totalTime();
        newSound.trimStart = 0;
        newSound.mediaObject = mediaObject;
        newSound.audioOutput = audioOutput;
        _sounds.append(newSound);

        emit soundCreated(msec, mediaObject->currentSource().fileName());
        emit static_cast<RobotActionTrackSignals*>(_wrapper)->itemCreated(msec);
    }

    //Update the duration of the soundtrack if needed.
    //This may need to be done asynchronously because totalTime may not
    //be immediately available.
    endTime = (newSound.timestamp + newSound.duration);
    if(endTime > _duration){
        _duration = endTime;
        qDebug() << "Updated duration on setting sound: " << _duration;
        emit trackDurationChanged(_duration, prevDuration);
    }
}

SoundInfo SoundTrack::deleteSound(int msec){
    int i, endTime;
    SoundObject removedSound;
    SoundInfo retValue;

    for(i = 0; i < _sounds.count(); i++){
        if(_sounds[i].timestamp == msec){
            removedSound = _sounds[i];
            endTime = (removedSound.duration + removedSound.timestamp);
            retValue.timestamp = removedSound.timestamp;
            retValue.duration = removedSound.duration;
            retValue.filename = new QString(removedSound.mediaObject->currentSource().fileName());

            delete removedSound.mediaObject;
            _sounds.remove(i);

            emit soundDeleted(msec);
            emit static_cast<RobotActionTrackSignals*>(_wrapper)->itemDeleted(msec);

            //Update the duration if needed.
            if(endTime == _duration){
                //This sound was the last sound. Update the duration.
                updateDuration();
                qDebug() << "Updated duration on sound deletion: " << _duration;
            }

            return retValue;
        }
    }

    retValue.timestamp = -1;
    retValue.duration = -1;
    retValue.filename = new QString("");
    return retValue;
}

void SoundTrack::moveSound(int msec, int newMsec){
    int i, endTime, prevDuration = _duration;

    for(i = 0; i < _sounds.count(); i++){
        if(_sounds[i].timestamp == msec){
            _sounds[i].timestamp = newMsec;

            endTime = (_sounds[i].timestamp + _sounds[i].duration);
            if(endTime > _duration){
                _duration = endTime;
                qDebug() << "Updated duration on sound move: " << _duration;
                emit trackDurationChanged(_duration, prevDuration);
            }

            emit soundMoved(msec, newMsec);
            emit static_cast<RobotActionTrackSignals*>(_wrapper)->itemMoved(newMsec, msec);
            return;
        }
    }
}

void SoundTrack::clipSound(int msec, int newDuration){
    int i, msecToEnd;

    for(i = 0; i < _sounds.count(); i++){
        if(_sounds[i].timestamp == msec){

            msecToEnd = (_sounds[i].mediaObject->totalTime() - newDuration);
            clipSound(_sounds[i], msecToEnd);
            return;
        }
    }
}

int SoundTrack::trimSound(int msec, int frontMsec, int backMsec){
    int i, frontTrimAmount = frontMsec, backTrimPosition;

    for(i = 0; i < _sounds.count(); i++){
        if(_sounds[i].timestamp == msec){
            //The standard editing behavior of trimming from the front
            //is to not shift the remaining audio from where it was.
            //This is accomplished by shifting both the start position
            //(in the media) and the timestamp (in the track) by the
            //given amount.

            //We can never trim to before the beginning of the media.
            //Correct so that the start position is never less than 0.
            if((_sounds[i].trimStart + frontTrimAmount) < 0)
                frontTrimAmount -= (_sounds[i].trimStart + frontTrimAmount);
            //We can never trim so far that we go "behind" the sound
            if(frontTrimAmount > (_sounds[i].duration - 10))
                frontTrimAmount = _sounds[i].duration - 10;

            _sounds[i].trimStart += frontTrimAmount;
            _sounds[i].timestamp += frontTrimAmount;
            emit soundMoved(_sounds[i].timestamp - frontTrimAmount, _sounds[i].timestamp);
            emit static_cast<RobotActionTrackSignals*>(_wrapper)->itemMoved(_sounds[i].timestamp, _sounds[i].timestamp - frontTrimAmount);
            _sounds[i].duration -= frontTrimAmount;

            //Trim from the back using the clipSound() function. It clips
            //the sound to a position relative to the duration of the media,
            //not the current sound duration.
            backTrimPosition = ((_sounds[i].duration + _sounds[i].trimStart) - backMsec);
            backTrimPosition = (_sounds[i].mediaObject->totalTime() - backTrimPosition);
            //Analogous to the front trim, the back trim cannot restore media that
            //doesn't exist.  backTrimPosition can never be less than 0.
            if(backTrimPosition < 0)
                backTrimPosition = 0;

            if(backTrimPosition > (_sounds[i].mediaDuration - _sounds[i].trimStart - 10)){
                backTrimPosition = _sounds[i].mediaDuration - _sounds[i].trimStart - 10;
            }

            clipSound(_sounds[i], backTrimPosition);

            return _sounds[i].timestamp;
        }
    }

    return -1;
}

void SoundTrack::clipTrack(int newDuration){
    int i, endTime, msecToEnd, prevDuration = _duration;

    //Clip every sound who's end time exceeds
    //the new duration
    for(i = 0; i < _sounds.count(); i++){
        endTime = (_sounds[i].timestamp + _sounds[i].duration);

        if(endTime > newDuration){
            //Clip the sound
            msecToEnd = (endTime - newDuration);
            clipSound(_sounds[i], msecToEnd);
        }
    }

    //By changing the duration, if there were sounds
    //who's duration wasn't known correctly, they should
    //clip when the total time is known (see onTotalTimeChanged)
    _duration = newDuration;

    emit trackDurationChanged(_duration, prevDuration);
}

void SoundTrack::moveAll(int msec){
    int i, prevTimestamp, firstSoundIndex, correctedShift = msec;

    //Find the "first" sound
    firstSoundIndex = 0;
    prevTimestamp = _duration;
    for(i = 0; i < _sounds.count(); i++){
        if(_sounds[i].timestamp < prevTimestamp){
            firstSoundIndex = i;
            prevTimestamp = _sounds[i].timestamp;
        }
    }
    //Correct the shift so that the earliest the first sound
    //can occur is at 0.
    if((_sounds[firstSoundIndex].timestamp + msec) < 0){
        correctedShift = -(_sounds[firstSoundIndex].timestamp);
        qDebug() << "corrected shift to: " << correctedShift;
    }

    //Shift all of the sounds based on the corrected shift
    if(correctedShift != 0){
        for(i = 0; i < _sounds.count(); i++){
            _sounds[i].timestamp += correctedShift;
            emit soundMoved(_sounds[i].timestamp - correctedShift, _sounds[i].timestamp);
            emit static_cast<RobotActionTrackSignals*>(_wrapper)->itemMoved(_sounds[i].timestamp, _sounds[i].timestamp - correctedShift);
        }

        //Update the duration of the sound track
        updateDuration();
    }
}


void SoundTrack::skip(int msec){
    int absPosition = currentTime() + msec;
    if(absPosition < 0)
        absPosition = 0;
    else if(absPosition > _duration)
        absPosition = _duration;

    setCurrentTime(absPosition);
}

SoundInfo SoundTrack::soundAt(int msec) const{
    int i;
    SoundInfo information;
    information.timestamp = msec;

    for(i = 0; i < _sounds.count(); i++){
        if(_sounds[i].timestamp == msec){
            information.duration = _sounds[i].duration;
            information.mediaDuration = _sounds[i].mediaDuration;
            information.trimStart = _sounds[i].trimStart;
            information.filename = new QString(_sounds[i].mediaObject->currentSource().fileName());
            return information;
        }
    }

    //We didn't find it
    information.timestamp = -1;
    information.duration = -1;
    information.mediaDuration = -1;
    information.trimStart = -1;
    information.filename = new QString("");
    return information;
}


QList<SoundInfo> SoundTrack::soundInformation() const{
    int i;
    SoundInfo information;
    QList<SoundInfo> retList;

    for(i = 0; i < _sounds.count(); i++){
        information.timestamp = _sounds[i].timestamp;
        information.duration = _sounds[i].duration;
        information.mediaDuration = _sounds[i].mediaDuration;
        information.trimStart = _sounds[i].trimStart;
        information.filename = new QString(_sounds[i].mediaObject->currentSource().fileName());

        retList.append(information);
    }

    return retList;
}



void SoundTrack::clipSound(SoundObject& sound, int msecToEnd){
    int prevEndTime;
    Phonon::MediaObject* mediaObject = sound.mediaObject;

    //We're going to use a trick to simulate the clipping of
    //a sound within the sound track.  The phonon media object
    //that is responsible for playing the sound still has the
    //entire sound file at its disposal.  However, we will set a
    //signal to be emitted when the clipped duration is reached,
    //which will tell the media object to stop playing.  The
    //clipped duration is held in the SoundObject struct and
    //should be used for calculations, not totalTime() from the
    //media object.
    if(msecToEnd >= 0){
        prevEndTime = (sound.timestamp + sound.duration);

        mediaObject->setPrefinishMark(msecToEnd);
        sound.duration = ((mediaObject->totalTime() - msecToEnd) - sound.trimStart);
        emit soundDurationChanged(sound.timestamp, sound.duration);

        if(prevEndTime == _duration){
            //This was the last clip; we need to update the
            //duration of the track
            updateDuration();
            qDebug() << "Updated duration on sound clipping: " << _duration;
        }
    }
}


void SoundTrack::updateDuration(){
    int i, curEndTime, greatestEndTime, prevDuration = _duration;

    //Cycle through the sounds and set the greatest ending time
    //as the duration of the soundtrack
    greatestEndTime = 0;
    for(i = 0; i < _sounds.count(); i++){
        curEndTime = (_sounds[i].timestamp + _sounds[i].duration);
        if(curEndTime > greatestEndTime)
            greatestEndTime = curEndTime;
    }

    _duration = greatestEndTime;
    emit trackDurationChanged(_duration, prevDuration);
}


void SoundTrack::onTotalTimeChanged(qint64 newTotalTime){
    int i, prevEndTime, msecToEnd;
    SoundObject sound;
    Phonon::MediaObject* source = ((Phonon::MediaObject*)sender());
    qDebug() << "On total time changed";

    //Find the sound object that contains the sending media object
    for(i = 0; i < _sounds.count(); i++){
        sound = _sounds[i];
        if(sound.mediaObject == source){
            prevEndTime = (sound.timestamp + sound.duration);
            _sounds[i].duration = newTotalTime;
            _sounds[i].mediaDuration = newTotalTime;
            emit soundDurationChanged(_sounds[i].timestamp, _sounds[i].duration);

            if(prevEndTime == _duration){
                //This was the last clip; we need to update
                //the track duration
                updateDuration();
                qDebug() << "Updated duration on total time changed: " << _duration;
            }
            else if(newTotalTime > _duration){
                //There was a call to clip the track, but we didn't
                //have the duration of this sound.  Clip the duration
                //so that it is no greater than the track duration
                msecToEnd = ((_sounds[i].timestamp + _sounds[i].duration) - _duration);
                clipSound(_sounds[i], msecToEnd);
            }
            return;
        }
    }
}


void SoundTrack::onPreFinishMark(qint32 msecToEnd){
    qDebug() << "Ending sound on clip";

    //Tell the source media object to stop playing
    Phonon::MediaObject* source = ((Phonon::MediaObject*)sender());
    source->stop();

    emit soundFinished(currentTime(), source->currentSource().fileName());
}

void SoundTrack::onFinished(){
    Phonon::MediaObject* source = ((Phonon::MediaObject*)sender());
    emit soundFinished(currentTime(), source->currentSource().fileName());
    qDebug() << "finished sound: " << source->currentSource().fileName();
}


void SoundTrack::onStateChanged(Phonon::State newState, Phonon::State oldState){

}

void SoundTrack::onDataReady(const QMap<Phonon::AudioDataOutput::Channel, QVector<qint16> > &data){
    //Dump the data into the buffer for the sound.
    qDebug() << "Data is ready";
}

